在任何一種程式語言都有資料型別介紹,而此篇我們將來了解 Kotlin 在資料型別上的特性、操作、轉換等內容。
在 Kotlin 官方文件中有提到:
In Kotlin, everything is an object in the sense that we can call member functions and properties on any variable.
上述內容得知,Kotlin 的任何東西都是一個物件,可以存取任何對象的相關方法與屬性,不像 Java 有區分原始型別(Primitive Type)
與參考型態(Reference Types)
,在開發上有時候甚至需要做轉換才可使用。而 Kotlin 在宣告變數時使用的是靜態類型系統(static type system)
,即編輯器會按照變數類型辨識程式碼,判斷是否有存在類型與數值不符合的狀況發生,若有出現,編輯器會立即指出,例如下圖提示訊息:
Kotlin 在變數宣告時主要會使用到兩種關鍵字 val
和 var
:
val
用於唯讀變數,一旦給值就無法再修改var
用於需要重新修改數值的情況fun main() {
val readOnlyVariable = "鐵人賽第十二屆" // 宣告一個唯讀變數
var playerName = "選手一號" // 宣告一個可重新修改數值的變數
playerName = "選手二號" // 重新賦予新數值
}
Kotlin 官方這邊也有建議開發者在開發上建議優先使用 val
,當遇到需要修改數值時再轉為 var
即可,若使用 var
宣告變數,開發者若沒有在程式中修改過,Intellij 編輯器也會提示建議改為 val
,如下圖:
還記得嗎?我們在上一章有提到 Kotlin 有一個優勢是可以避免以前 Java 開發中常見的 NullPointerException
情況發生,主要原因是因為 Kotlin 預設宣告都只能是非 null
型態,例如以下範例,當我們想要進行指派 null
值給 String 時會發生編譯錯誤狀況:
這樣的錯誤檢查就能夠避免開發者經常會有出現錯誤的問題,而如果在開發情境上確實有必要使用 null
值,則可以將變數定義為 nullable
狀態,即在變數的型態定義上加上 ?
即可,如下範例:
fun main() {
var test: String? = "鐵人賽"
test = null
println(test) // 印出 null
}
在介紹基本型別前,先介紹 Kotlin 在變數上有個特色是型別判斷處理,可對於已指派預設值的宣告變數自動定義型別,允許開發者省略型別定義,以下我們嘗試宣告一個變數,並輸出該變數的型別來看 Kotlin 是否有自動幫我們進行型別宣告,如下範例:
此範例先宣告變數 name 為「鐵人賽」,再利用「::class.simpleName」印出變數型別結果為 String
fun main() {
val name = "鐵人賽"
println(name::class.simpleName) // 印出 String -> 代表 Kotlin 自動幫我們定義型態
}
Kotlin 在資料型別與 Java 非常相似,只差在變數型態必須使用首字大寫
,型別分別如下:
數值型別 Numbers (種類可依長度區分)
數值變數在操作上可直接宣告型態或是透過型別判斷進行操作:
fun main() {
val byte: Byte = 1
val short: Short = 2
val int: Int = 3
val long: Long = 4L
val float: Float = 5f
val double: Double = 6.0
println("Byte => $byte")
println("Short => $short")
println("Int => $int")
println("Long => $long")
println("Float => $float")
println("Double => $double")
}
前面有提到 Kotlin 的一切都是物件,在以前 Java 變數型態有分為基本型別(Primitive type)
與參考型別(Reference type)
,即 int
與 Integer
的差別,而在 J2SE 5.0
時有提供自動裝箱(autoboxing)
與拆箱(unboxing)
來進行包裹基本型態,但在 Kotlin 中,只存在數值的裝箱,不存在拆箱,因為 Kotlin 是沒有存在基本資料型態的,下面將示範如何進行裝箱操作:
此範例操作須搭配上面提到的概念-空值型態達成裝箱效果,會發現裝箱前與裝箱後的數值都一樣
fun main() {
val number: Int = 913
val numberInBox: Int? = number
println("裝箱前數值: $number , 裝箱後數值: $numberInBox")
// 裝箱前數值: 913 , 裝箱後數值: 913
}
上面範例我們會發現兩個數值印出來雖然是相等的,但其實在 Kotlin 判斷數值是否相等有兩種比較方式(==
與 ===
),==
是判斷數值是否相等, ===
則是判斷兩個數值在記憶體位置是否相等,而其實 Kotlin 在變數裝箱操作時,記憶體位置會根據其資料型別的數值範圍進行定義,我們可以利用下面範例進行示範:
我們會發現當 a 變數為 127 時,判斷兩個裝箱變數會為 true
,因為 Int 型態定義數值範圍為 -128 ~ 127
,當 b 變數超過 127 數值時,Kotlin 在記憶體分配上會有不同位置狀況發生。
fun main() {
val a: Int = 127
val boxedA: Int? = a
val anotherBoxedA: Int? = a
val b: Int = 128
val boxedB: Int? = b
val anotherBoxedB: Int? = b
println(boxedA === anotherBoxedA) // true
println(boxedB === anotherBoxedB) // false
}
Kotlin 在數值轉換上有分顯性轉換與隱性轉換,隱性轉換即 Kotlin 會自動幫我們進行轉換,但若兩個數值為不同型態時,會自動以定義數值範圍較大的型態為轉換後的最終型態,例如以下範例:
此範例為兩數相加,999為 Long
型態,1為 Int
型態,兩數相加後的結果 number 為 Long
型態
fun main() {
val number = 999L + 1
println(number::class.simpleName) // 印出資料型別為 Long
}
而為了避免隱性轉換時自動選擇型態問題,我們在開發上可使用顯性轉換方式,即下面範例:
fun main() {
val number: Int = 65
println(number.toByte()) // 印出 65
println(number.toShort()) // 印出 65
println(number.toLong()) // 印出 65
println(number.toFloat()) // 印出 65.0
println(number.toDouble()) // 印出 65.0
println(number.toChar()) // 印出 A
println(number.toString()) // 印出 65
}
字元型別 Char
Char 表示字元類型,字元變數必須使用單引號(‘’)表示,在轉換上可利用顯性轉換為數字型態,如以下範例:
fun main() {
val char: Char = 'A'
println(char.toInt()) // 印出 65
}
字串型別 String
String 表示字串類型,在輸出時可使用字串模板表示式處理字串組成,再進行輸出,如下範例:
fun main() {
val username: String = "Devin"
println("第十二屆鐵人賽 參加者 $username") // 印出「第十二屆鐵人賽 參加者 Devin」
}
布林型別 Boolean
Boolean 表示為布林類型,其值有 true
與 false
fun main() {
val isFalse: Boolean = false
val isTrue: Boolean = true
println(isFalse && isTrue) // 印出「false」
}
陣列型別 Array
Kotlin 的 Array 型別在宣告上是以 Array<T>
表示,我們可以到 Kotlin 的 Array
型態定義查看,會發現原始型態已經幫我們定義 get、set、size 與 iterator 方法:
故我們在 Array 操作上可以如下範例進行操作:
fun main() {
val data: Array<Int> = arrayOf(1,2,3,4,5) // 宣告Array並賦予 1-5 數值
data.forEach { println(it) } // 利用 forEach 分別印出數值
}
在前述有提到唯讀變數 val
不允許重新設定數值,但其實 val
是在程式執行階段(Run time)才進行賦值(Assign Value)
動作,而我們若要限制程式在編輯階段(Compile time)就進行賦值動作,應使用 const
關鍵字搭配 val
進行變數宣告,我們可用一個範例來說明 const
與 val
的差異:
透過上面範例我們會發現兩件事:
normalVariable
可利用 getRandomValue() 隨機取得 1 - 6 數值,表示程式是先在執行階段利用 getRandomValue() 方法取得數值後,才對 normalVariable
進行賦值constVariableFromGetValue
賦予 getRandomValue 方法時,會出現 const val 只能接受常數(constant value)is 運算子
is運算子可檢查物件或變數是否屬於某資料型別,如Int、String等,類似於Java的 instanceof
fun main() {
val data = "abc"
println(data is String); // 印出 true
println(data is Any); // 印出 true
}
as 運算子進行型別轉換
as運算子用於型別轉換,若要轉換的數值與指定型別相容,轉換就會成功;如果型別不相容,使用 as? 運算子就會返回值null,如下範例:
fun main() {
val x: Int = 2
val y: Int = x as Int
val z: String? = y as? String
println(y) // 印出 2
println(z) // 印出 null
}
除了上述基本型別以外,Kotlin 還有一些特殊型別運用於物件或函數上,這邊會先進行簡單介紹,會在後續章節介紹時會再深入說明:
根據 Kotlin 官方文件所述:
The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
在此篇文章一開始介紹說明,Kotlin的一切都是物件,而每個物件其實都是繼承 Any 這個型別,此型別相當於Java的 Object 型別,而此型別也可再細分為 Any 與 Any?,Any屬於非空型別的根物件,Any?屬於可空型別的根物件。
在 Java 中,當我們所設計的 function 不需回傳值時,我們會使用到 void 型別,而在 Kotlin 可使用 Unit 型別代替,而且若我們不特地為 function 設定回傳型態時,Kotlin 會自動幫我們預設型態為 Unit 型別,會返回 Unit 型別,例如以下範例。
fun main() {
val username = getUserName()
println(username::class.simpleName) // 印出 Unit 型別
}
fun getUserName() {
}
Nothing 型別其實類似於 Unit,Nothing 型別也是不返回任何東西,但差別在於 Nothing 型別意味著此函數不可能成功執行完成,只會拋出異常或是再也回不去函數呼叫的地方。
而 Nothing? 型別則會有一個使用情境,在 Java 中,void不能是變數的型別。也不能被當數值列印輸出。但是,在Java中有個包裝類Void是 void 的自動裝箱型別,如果我們想讓 function 返回型別永遠是 null 的話,可以把返回型別置為這個大寫的V的Void型別,而 Void 即對應 Kotlin 中的 Nothing? 型別。
範例(1) 使用 Nothing 型別
fun main() {
getUserName() // 使用 Nothing 型別
}
fun getUserName(): Nothing {
throw NotImplementedError() // 丟出異常
}
範例(2) 使用 Nothing? 型別
fun main() {
getUserName() // 使用 Nothing? 型別
}
fun getUserName(): Nothing? {
return null // 保持回傳 null
}
有時候我們可能會好奇在 Kotlin 所撰寫的程式,實際轉換為 Java 會是怎麼樣的語法,此時我們可以利用 intellij 內建的工具進行轉換觀察。
在 Intellij 連續按 Shift 鍵兩次,搜尋「show kotlin」關鍵字,選擇「Show Kotlin Bytecode」,會出現Kotlin位元組碼工具視窗,再點擊「Decompile」按鈕即可觀看轉譯的Java 程式碼。